package redis

import (
	"encoding/json"
	"fmt"
	"github.com/edgexfoundry/edgex-go/internal/app/models"
	"github.com/edgexfoundry/edgex-go/internal/app/pkg/controller/http"
	pkgCommon "github.com/edgexfoundry/edgex-go/internal/pkg/common"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/errors"
	"github.com/gomodule/redigo/redis"
	"github.com/google/uuid"
	"strings"
	"time"
)

const (
	DeviceCommandCollection                  = DeviceCollection + DBKeySeparator + common.Command
	DeviceCommandNameCollection              = DeviceCollection + DBKeySeparator + common.CommandName
	DeviceCommandResourceOperationCollection = DeviceCollection + DBKeySeparator + common.Command + DBKeySeparator + OperationResource
	DeviceProfileCommandCollection           = DeviceCollection + DBKeySeparator + common.Profile + DBKeySeparator + common.Command
	DeviceProfileCommandNameCollection       = DeviceCollection + DBKeySeparator + common.Profile + DBKeySeparator + common.CommandName
	DeviceResourceOperation                  = DeviceCollection + DBKeySeparator + OperationResource
	DeviceResourceResourceOperation          = DeviceCollection + DBKeySeparator + OperationResource + DBKeySeparator + common.Resource
)

func checkDeviceCommandExist(conn redis.Conn, id string, code string) errors.EdgeX {
	exists, err := deviceCommandExists(conn, id, code)
	if err != nil {
		return errors.NewCommonEdgeX(errors.Kind(err), http.DataBaseQueryError, err)
	} else if !exists {
		return errors.NewCommonEdgeX(errors.KindInvalidId, fmt.Sprintf(http.DeviceCommandNotExist, code), nil)
	}
	return nil
}

// deviceCommandExists checks whether the device command exists by code
func deviceCommandExists(conn redis.Conn, deviceId, code string) (bool, errors.EdgeX) {
	exists, err := objectIdExists(conn, CreateKey(DeviceCommandCollection, deviceId, code))
	if err != nil {
		return false, errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceCommandNotExist, code), err)
	}
	return exists, nil
}

// deviceCommandGetByCode device command get by code
func deviceCommandGetByCode(conn redis.Conn, deviceId string, code string) (models.DeviceCommand, errors.EdgeX) {
	var command models.DeviceCommand
	storedKey := CreateKey(DeviceCommandCollection, deviceId, code)
	edgeXerr := getObjectById(conn, storedKey, &command)
	if edgeXerr != nil {
		return command, errors.NewCommonEdgeXWrapper(edgeXerr)
	}
	return command, nil
}

// deviceCommandAdd device command add
func deviceCommandAdd(conn redis.Conn, deviceId string, dcs ...models.DeviceCommand) (string, errors.EdgeX) {

	deviceCommandKey := CreateKey(DeviceProfileCommandCollection, deviceId)

	deviceResourceName := CreateKey(DeviceProfileResourceNameCollection, deviceId)
	deviceCommandName := CreateKey(DeviceProfileCommandNameCollection, deviceId)

	commandExistIds, commandExistNames, resourceExistNames, errRedis := handlerQueryCommandExistInfo(conn, deviceCommandKey, deviceCommandName, deviceResourceName)
	if errRedis != nil {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, "query object names from database failed", errRedis)
	}

	commandNames := make([]string, len(dcs))
	for index, command := range dcs {

		//功能名称不可为空
		name := command.Name
		if name == "" {
			return "", errors.NewCommonEdgeX(errors.KindContractInvalid, http.CommandNameNotNull, nil)
		}

		//调整Command业务key为UUID，同时校验功能名称是否重复
		storedKey := CreateKey(DeviceCommandCollection, deviceId, command.Id)
		if pkgCommon.IsStringsContain(commandExistIds, storedKey) {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.CommandIdAlreadyExist, command.Id), nil)
		}

		nameKey := CreateKey(DeviceCommandNameCollection, deviceId, command.Name)

		//功能名称不可重复
		if pkgCommon.IsStringsContain(commandExistNames, nameKey) {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.CommandNameAlreadyExist, command.Name), nil)
		}

		//属性名称不可重复
		if pkgCommon.IsStringsContain(resourceExistNames, CreateKey(DeviceResourceNameCollection, deviceId, command.Name)) {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNameAlreadyExist, command.Name), nil)
		}

		commandWithoutResource := command
		commandWithoutResource.ResourceOperations = nil

		ts := pkgCommon.MakeTimestamp()
		commandWithoutResource.Created = ts
		commandWithoutResource.Modified = ts

		r, err := json.Marshal(commandWithoutResource)
		if err != nil {
			return "", errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", nil)
		}

		err = deviceCommandOperationAdd(conn, deviceId, command.Id, command.ResourceOperations...)
		if err != nil {
			return "", errors.NewCommonEdgeXWrapper(err)
		}

		commandNames[index] = command.Name
		_ = conn.Send(SET, storedKey, r)
		_ = conn.Send(ZADD, deviceCommandKey, time.Now().UnixMilli(), storedKey)

		_ = conn.Send(SET, nameKey, r)
		_ = conn.Send(ZADD, deviceCommandName, time.Now().UnixMilli(), nameKey)
	}

	return strings.Join(commandNames, ","), nil
}

func handlerQueryCommandExistInfo(conn redis.Conn, deviceCommandKey string, deviceCommandName string, deviceResourceName string) ([]string, []string, []string, error) {
	var commandExistIds, commandExistNames, resourceExistNames []string
	commandExists, _ := objectIdExists(conn, deviceCommandKey)

	//查询功能id及name
	if commandExists {
		ids, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceCommandKey, 0, -1))
		if errRedis != nil {
			return nil, nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", errRedis)
		}
		commandExistIds = ids

		names, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceCommandName, 0, -1))
		if errRedis != nil {
			return nil, nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", errRedis)
		}
		commandExistNames = names
	}

	//查询属性名称集合
	resourceExists, _ := objectIdExists(conn, deviceResourceName)
	if resourceExists {
		names, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceResourceName, 0, -1))
		if errRedis != nil {
			return nil, nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", errRedis)
		}
		resourceExistNames = names
	}
	return commandExistIds, commandExistNames, resourceExistNames, nil
}

func deviceCommandOperationAdd(conn redis.Conn, deviceId, commandId string, ros ...models.ResourceOperation) errors.EdgeX {

	commandOperationResourceKey := CreateKey(DeviceCommandResourceOperationCollection, deviceId, commandId)

	rorKey := CreateKey(DeviceResourceResourceOperation, deviceId)

	for _, resourceOperation := range ros {

		uid, _ := uuid.NewUUID()
		id := uid.String()
		roKey := CreateKey(DeviceResourceOperation, id)

		ro := resourceOperation
		ro.UUID = id
		r, err := json.Marshal(ro)
		if err != nil {
			return errors.NewCommonEdgeX(errors.KindContractInvalid, http.ParamsError, nil)
		}

		resourceKey := CreateKey(DeviceResourceCollection, deviceId, resourceOperation.UUID)
		//修改功能时，其关联属性会先删后增，原功能中属性UUID已重新升成，导致其与原属性UUID不一致，会无法通过校验，SO此处需调整校验逻辑，通过name唯一性校验是否存在该属性
		/*exist, err := objectIdExists(conn, resourceKey)
		if err != nil || !exist {
			return errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf(http.DeviceResourceNotExist, resourceOperation.UUID), nil)
		}*/
		nameKey := CreateKey(DeviceResourceNameCollection, deviceId, resourceOperation.DeviceResource)
		exist, err := objectIdExists(conn, nameKey)
		if err != nil || !exist {
			return errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf(http.DeviceResourceNotExist, resourceOperation.DeviceResource), nil)
		}

		_ = conn.Send(SET, roKey, r)
		_ = conn.Send(ZADD, commandOperationResourceKey, time.Now().UnixMilli(), roKey)

		_ = conn.Send(HSET, rorKey, roKey, resourceKey)
	}
	return nil
}

func deviceCommandGet(conn redis.Conn, deviceId string, offset, limit int) ([]models.DeviceCommand, errors.EdgeX) {

	deviceCommandKey := CreateKey(DeviceProfileCommandCollection, deviceId)
	objects, err := getObjectsByRange(conn, deviceCommandKey, offset, limit)
	if errors.Kind(err) == errors.KindEntityDoesNotExist {
		return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, http.ParamsError, nil) // Empty Readings in an Event is not an error
	} else if err != nil {
		return nil, errors.NewCommonEdgeXWrapper(err)
	}

	commands := make([]models.DeviceCommand, len(objects))
	for index, obj := range objects {
		var command models.DeviceCommand
		err := json.Unmarshal(obj, &command)
		if err != nil {
			continue
		}

		resourceOperations, err := resourceOperationsGet(conn, deviceId, command.Id)
		if err != nil {
			return nil, errors.NewCommonEdgeX(errors.KindDatabaseError, http.DataBaseQueryError, nil)
		}
		command.ResourceOperations = resourceOperations
		commands[index] = command
	}

	return commands, nil
}

func resourceOperationsGet(conn redis.Conn, deviceId, commandId string) (resourceOperations []models.ResourceOperation, err errors.EdgeX) {

	commandOperationResourceKey := CreateKey(DeviceCommandResourceOperationCollection, deviceId, commandId)

	objects, err := getObjectsByRange(conn, commandOperationResourceKey, 0, -1)
	if errors.Kind(err) == errors.KindEntityDoesNotExist {
		return // Empty Readings in an Event is not an error
	} else if err != nil {
		return resourceOperations, errors.NewCommonEdgeXWrapper(err)
	}

	resourceOperations = make([]models.ResourceOperation, len(objects))

	for index, obj := range objects {
		var operation models.ResourceOperation
		err := json.Unmarshal(obj, &operation)
		if err != nil {
			continue
		}

		deviceResource, err := getDeviceResourceByResourceOperation(conn, deviceId, operation.UUID)
		operation.ResourceCode = deviceResource.Code
		operation.DeviceResource = deviceResource.Name
		operation.Resource = deviceResource
		resourceOperations[index] = operation
	}
	return
}

func getDeviceResourceByResourceOperation(conn redis.Conn, deviceId, uuid string) (deviceResource models.DeviceResource, edgeXErr errors.EdgeX) {
	if uuid == "" {
		return
	}

	hashStoredKey := CreateKey(DeviceResourceResourceOperation, deviceId)
	operationKey := CreateKey(DeviceResourceOperation, uuid)
	edgeXErr = getObjectByHash(conn, hashStoredKey, operationKey, &deviceResource)
	if edgeXErr != nil {
		return deviceResource, errors.NewCommonEdgeX(errors.Kind(edgeXErr), http.DataBaseQueryError, edgeXErr)
	}
	return
}

func deviceCommandDelete(conn redis.Conn, deviceId, commandId string) errors.EdgeX {
	deviceCommandKey := CreateKey(DeviceProfileCommandCollection, deviceId)
	deviceCommandName := CreateKey(DeviceProfileCommandNameCollection, deviceId)

	commandStoreKey := CreateKey(DeviceCommandCollection, deviceId, commandId)

	//通过功能key获取功能name并判空
	var command models.DeviceCommand
	err := getObjectById(conn, commandStoreKey, &command)

	if err != nil {
		return errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, fmt.Sprintf(http.DeviceCommandNotExist, commandId), nil)
	}

	//获取到用于唯一性校验key
	commandStoreNameKey := CreateKey(DeviceCommandNameCollection, deviceId, command.Name)

	_ = conn.Send(ZREM, deviceCommandKey, commandStoreKey)
	_ = conn.Send(DEL, commandStoreKey)

	//同步清理唯一性校验key
	_ = conn.Send(ZREM, deviceCommandName, commandStoreNameKey)
	_ = conn.Send(DEL, commandStoreNameKey)

	commandOperationResourceKey := CreateKey(DeviceCommandResourceOperationCollection, deviceId, commandId)

	operations, _ := resourceOperationsGet(conn, deviceId, commandId)
	for _, opt := range operations {
		uid := opt.UUID
		hashStoredKey := CreateKey(DeviceResourceResourceOperation, deviceId)
		operationKey := CreateKey(DeviceResourceOperation, uid)
		_ = conn.Send(DEL, operationKey)
		_ = conn.Send(HDEL, hashStoredKey, operationKey)
	}

	_ = conn.Send(DEL, commandOperationResourceKey)
	return nil
}

// deviceCommandUpdate device command add
func deviceCommandUpdate(conn redis.Conn, deviceId string, command models.DeviceCommand) errors.EdgeX {

	deviceCommandKey := CreateKey(DeviceProfileCommandCollection, deviceId)

	deviceResourceName := CreateKey(DeviceProfileResourceNameCollection, deviceId)
	deviceCommandName := CreateKey(DeviceProfileCommandNameCollection, deviceId)

	commandExistIds, commandExistNames, resourceExistNames, errRedis := handlerQueryCommandExistInfo(conn, deviceCommandKey, deviceCommandName, deviceResourceName)
	if errRedis != nil {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, "query object names from database failed", errRedis)
	}

	storedKey := CreateKey(DeviceCommandCollection, deviceId, command.Id)
	if !pkgCommon.IsStringsContain(commandExistIds, storedKey) {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceCommandNotExist, command.Id), nil)
	}

	origin, err := deviceCommandGetByCode(conn, deviceId, command.Id)
	if err != nil {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.CommandIdNotExist, command.Id), err)
	}

	//比较名字是否被占用，需排除自身（即未修改名称的情况）
	nameKey := CreateKey(DeviceCommandNameCollection, deviceId, command.Name)
	oldNameKey := CreateKey(DeviceCommandNameCollection, deviceId, origin.Name)

	if pkgCommon.IsStringsContain(commandExistNames, nameKey) && origin.Name != command.Name {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.CommandNameAlreadyExist, command.Name), nil)
	}

	//是否占用属性名字
	if pkgCommon.IsStringsContain(resourceExistNames, CreateKey(DeviceResourceNameCollection, deviceId, command.Name)) {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNameAlreadyExist, command.Name), nil)
	}

	origin.Code = command.Code
	origin.Name = command.Name
	origin.IsHidden = command.IsHidden
	origin.Description = command.Description

	ts := pkgCommon.MakeTimestamp()
	origin.Modified = ts

	r, errJson := json.Marshal(origin)
	if errJson != nil {
		return errors.NewCommonEdgeX(errors.KindContractInvalid, http.UnknownError, nil)
	}

	err = deviceCommandOperationDelete(conn, deviceId, command.Id)
	if err != nil {
		return errors.NewCommonEdgeXWrapper(err)
	}

	err = deviceCommandOperationAdd(conn, deviceId, command.Id, command.ResourceOperations...)
	if err != nil {
		return errors.NewCommonEdgeXWrapper(err)
	}

	//处理name校验key，删掉原key，增加新key
	_ = conn.Send(ZREM, deviceCommandName, oldNameKey)
	_ = conn.Send(DEL, oldNameKey)

	_ = conn.Send(SET, nameKey, r)
	_ = conn.Send(ZADD, deviceCommandName, time.Now().UnixMilli(), nameKey)

	_ = conn.Send(SET, storedKey, r)
	_ = conn.Send(ZADD, deviceCommandKey, time.Now().UnixMilli(), storedKey)

	return nil
}

func deviceCommandOperationDelete(conn redis.Conn, deviceId, commandId string) errors.EdgeX {

	commandOperationResourceKey := CreateKey(DeviceCommandResourceOperationCollection, deviceId, commandId)

	rorKey := CreateKey(DeviceResourceResourceOperation, deviceId)

	keys, err := fieldsByZSet(conn, commandOperationResourceKey)
	if err != nil {
		return errors.NewCommonEdgeXWrapper(err)
	}
	for _, field := range keys {
		_ = conn.Send(ZREM, commandOperationResourceKey, field)
		_ = conn.Send(HDEL, rorKey, field)
	}
	return nil
}

func deleteAllCommands(conn redis.Conn, deviceId string) errors.EdgeX {
	deviceCommandKey := CreateKey(DeviceProfileCommandCollection, deviceId)

	fields, _ := fieldsByZSet(conn, deviceCommandKey)

	for _, field := range fields {
		roptKey := strings.Replace(field, DeviceCommandCollection, DeviceCommandResourceOperationCollection, 1)
		optFields, _ := fieldsByZSet(conn, roptKey)

		_ = conn.Send(DEL, strings.Join(fields, " "))
		hashOptsKey := CreateKey(DeviceResourceResourceOperation, deviceId)
		_ = conn.Send(HDEL, hashOptsKey, optFields)
		_ = conn.Send(DEL, hashOptsKey)
		_ = conn.Send(DEL, roptKey)
	}

	_ = conn.Send(DEL, strings.Join(fields, " "))
	_ = conn.Send(DEL, deviceCommandKey)
	return nil
}
